# coding:utf-8
import numpy as np
import pandas as pd

# 柯里化一个print
printn = lambda *x: print(*x, sep='\n')


# 背景和目的--表中的⼀列通常会有重复的包含不同值的⼩集合的情况
def background():
    values = pd.Series(['apple', 'orange', 'apple', 'apple'] * 2)
    printn('原始数据:', values, '唯一值:', pd.unique(values), '数值计数:',
           pd.value_counts(values))
    '''
    许多数据系统（数据仓库、统计计算或其它应⽤）都发展出了特定的表征重复值的⽅法，以进⾏⾼效的存储和计算。
    在数据仓库中，最好的⽅法是使⽤所谓的包含不同值的维表(Dimension Table)，将主要的参数存储为引⽤维表整数键：
    '''
    values = pd.Series([0, 1, 0, 0] * 2)
    dim = pd.Series(['apple', 'orange'])
    # 可以使⽤take⽅法存储原始的字符串Series
    s = dim.take(values)
    printn('take方法存储:', s)
    # 这种⽤整数表示的⽅法称为分类表示法。表示分类的整数值称为分类编码或简单地称为编码
    '''
    分类表示可以在进⾏分析时⼤⼤的提⾼性能。你也可以在保持编码不变的情况下，对分类进⾏转换。
    ⼀些相对简单的转变例⼦包括：
        1、重命名分类。
        2、加⼊⼀个新的分类，不改变已经存在的分类的顺序或位置。
    '''


# background()


# pandas的分类类型--分类数组可以包括任意不可变类型
# pandas有⼀个特殊的分类类型，⽤于保存使⽤整数分类表示法的数据
def classification():
    fruits = ['apple', 'orange', 'apple', 'apple'] * 2
    N = len(fruits)
    df = pd.DataFrame(
        {
            'fruit': fruits,
            'basket_id': np.arange(N),
            'count': np.random.randint(3, 15, size=N),
            'weight': np.random.uniform(0, 4, size=N)
        },
        columns=['basket_id', 'fruit', 'count', 'weight'])
    printn('设定的DF:', df)

    # df['fruit']是⼀个Python字符串对象的数组。我们可以通过调⽤它，将它转变为分类
    fruit_cat = df['fruit'].astype('category')
    # fruit_cat的值不是NumPy数组，⽽是⼀个pandas.Categorical实例
    c = fruit_cat.values
    printn('分类:', fruit_cat, '值的类型:', type(c))
    # 分类对象有categories和codes属性
    printn('categories属性:', c.categories, 'codes属性:', c.codes)

    # 可将DataFrame的列通过分配转换结果，转换为分类
    df['fruit'] = df['fruit'].astype('category')
    # 还可以从其它Python序列直接创建pandas.Categorical
    my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
    printn('两种分类的生成方式:', df['fruit'], my_categories)
    # 如果你已经从其它源获得了分类编码，你还可以使⽤from_codes构造器
    categories = ['foo', 'bar', 'baz']
    codes = [0, 1, 2, 0, 0, 1]
    my_cats_2 = pd.Categorical.from_codes(codes, categories)
    printn("from_codes生成的分类:", my_cats_2)
    '''
    与显示指定不同，分类变换不认定指定的分类顺序。因此取决于输⼊数据的顺序，categories数组的顺序会不同。
    当使⽤from_codes或其它的构造器时，你可以指定分类⼀个有意义的顺序
    '''
    ordered_cat = pd.Categorical.from_codes(codes, categories, ordered=True)
    printn('指定顺序的Categories:',
           ordered_cat)  # 输出[foo < bar < baz]指明‘foo’位于‘bar’的前⾯
    # ⽆序的分类实例可以通过as_ordered排序
    ordered_cat_2 = my_cats_2.as_ordered()
    printn('as_ordered生成顺序:', ordered_cat_2)


# classification()


# ⽤分类进⾏计算
def classcal():
    # 使⽤pandas.qcut⾯元函数。它会返回pandas.Categorical
    np.random.seed(12345)
    draws = np.random.randn(1000)
    printn('生成的随机数组:', draws[:5])
    # 计算这个数据的分位⾯元，提取⼀些统计信息
    bins0 = pd.qcut(draws, 4)
    # 虽然有⽤，确切的样本分位数与分位的名称相⽐，不利于⽣成汇总
    printn('分位面元:', bins0)
    # 可以使⽤labels参数qcut，实现⽬的
    bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
    printn('分位面元:', bins, 'codes属性', bins.codes[:10])

    # 加上标签的⾯元分类不包含数据⾯元边界的信息，因此可以使⽤groupby提取⼀些汇总信息
    bins = pd.Series(bins, name='quartile')
    results = (pd.Series(draws).groupby(bins).agg(['count', 'min',
                                                   'max']).reset_index())
    # 分位数列保存了原始的⾯元分类信息，包括排序
    printn('groupby提取的汇总信息:', results, '面元分类信息:', results['quartile'])


# classcal()


# ⽤分类提⾼性能
# 如果你是在⼀个特定数据集上做⼤量分析，将其转换为分类可以极⼤地提⾼效率。DataFrame列的分类使⽤的内存通常少的多
def ClassEnhance():
    N = 10000000
    draws = pd.Series(np.random.randn(N))
    labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))
    # 将标签转换为分类
    categories = labels.astype('category')
    # 看看标签和分类的内存使用
    printn('标签的内存使用:', labels.memory_usage(), '分类的内存使用:',
           categories.memory_usage())
    # 转换为分类不是没有代价的，但这是⼀次性的代价,astype需要时间
    # GroupBy使⽤分类操作明显更快，是因为底层的算法使⽤整数编码数组，⽽不是字符串数组
    # ClassEnhance()


# 分类方法
# 包含分类数据的Series有⼀些特殊的⽅法，类似于Series.str字符串⽅法。它还提供了⽅便的分类和编码的使⽤⽅法
def classimethod():
    '''
    可用的分类方法：
    add_categories--在已存在的分类后面添加新的(未使用的)分类
    as_ordered--使分类有序
    as_unordered--使分类无序
    remove_categories--移除分类，设置任何被移除的值为null
    remove_unused_categories--移除任意不出现在数据中的分类值
    rename_categories--用指定的新分类的名字替换分类；不能改变分类的数目
    reorder_categories--与rename_categories很想，但是可以改变结果，使分类有序
    set_categories--用指定的新分类的名字替换分类；可以调价或删除分类
    '''
    s = pd.Series(['a', 'b', 'c', 'd'] * 2)
    cat_s = s.astype('category')
    printn('转化为分类:', cat_s)
    # 特别的cat属性提供了分类⽅法的⼊⼝
    printn('cat的codes属性:', cat_s.cat.codes, 'cat的categories属性:',
           cat_s.cat.categories)
    # 假设我们知道这个数据的实际分类集，超出了数据中的四个值。我们可以使⽤set_categories⽅法改变它们
    actual_categories = ['a', 'b', 'c', 'd', 'e']
    cat_s2 = cat_s.cat.set_categories(actual_categories)
    printn('新的分类集:', cat_s2, '旧的value_counts:', cat_s.value_counts(),
           '新的value_counts:', cat_s2.value_counts())
    '''
    在⼤数据集中，分类经常作为节省内存和⾼性能的便捷⼯具。过滤完⼤DataFrame或Series之后，许多分类可能不会出现在数据中。我们可以使⽤remove_unused_categories⽅法删除没看到的分类
    '''
    cat_s3 = cat_s[cat_s.isin(['a', 'b'])]
    printn('存在没使用的分类数据:', cat_s3)
    cat_s4 = cat_s3.cat.remove_unused_categories()
    printn('remove_unused_categories后:', cat_s4)


# classimethod()

# 为建模创建虚拟变量
'''
当你使⽤统计或机器学习⼯具时，通常会将分类数据转换为虚拟变量，也称为one-hot编码。
这包括创建⼀个不同类别的列的DataFrame；这些列包含给定分类的1s，其它为0。
'''


def createvirtual():
    cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')
    # pandas.get_dummies函数可以转换这个分类数据为包含虚拟变量的DataFrame
    ss = pd.get_dummies(cat_s)
    printn('包含虚拟变量的DataFrame:', ss)


# createvirtual()

# GroupBy高级应用
# 分组转换和“解封”GroupBy
'''
我们在分组操作中学习了apply⽅法，进⾏转换。
还有另⼀个transform⽅法，它与apply很像，但是对使⽤的函数有⼀定限制：
    1、它可以产⽣向分组形状⼴播标量值
    2、它可以产⽣⼀个和输⼊组形状相同的对象
    3、它不能修改输⼊
'''


def transtest():
    df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4, 'value': np.arange(12.)})
    # 按键进⾏分组
    g = df.groupby('key').value
    printn('原DF:', df, '按key分组后均值:', g.mean())

    # 假设我们想产⽣⼀个和df['value']形状相同的Series，但值替换为按键分组的平均值。
    # 我们可以传递函数lambda x: x.mean()进⾏转换
    g1 = g.transform(lambda x: x.mean())
    printn('transform后:', g1)
    # 对于内置的聚合函数，我们可以传递⼀个字符串假名作为GroupBy的agg⽅法
    g2 = g.transform('mean')
    printn('transform后2:', g2)

    # 与apply类似，transform的函数会返回Series，但是结果必须与输⼊⼤⼩相同
    # 例如:我们可以⽤lambda函数将每个分组乘以2
    g3 = g.transform(lambda x: x * 2)
    printn('transform后3:', g3)

    # 举⼀个复杂的例⼦，我们可以计算每个分组的降序排名
    g4 = g.transform(lambda x: x.rank(ascending=False))
    printn('transform后4:', g4)

    # ⼀个由简单聚合构造的的分组转换函数
    def normalize(x):
        return (x - x.mean()) / x.std()

    # ⽤transform或apply可以获得等价的结果
    g5 = g.transform(normalize)
    g6 = g.apply(normalize)
    printn('transform后5:', g5, 'apply后:', g6)
    '''
    内置的聚合函数，⽐如mean或sum，通常⽐apply函数快，也⽐transform快。
    这允许我们进⾏⼀个所谓的解封（unwrapped）分组操作
    '''
    normalized = (df['value'] - g.transform('mean')) / g.transform('std')
    printn('unwrapped操作和自定义函数传递apply的结果一样,但速度更快:', normalized)


# transtest()


# 分组的时间重采样
# 对于时间序列数据，resample⽅法从语义上是⼀个基于内在时间的分组操作
def timegroup():
    N = 15
    times = pd.date_range('2017-05-20 00:00', freq='1min', periods=N)
    df = pd.DataFrame({'time': times, 'value': np.arange(N)})
    printn('设定的DF:', df)

    # ⽤time作为索引，然后重采样
    df1 = df.set_index('time').resample('5min').count()
    printn('重采样结果:', df1)

    # 假设DataFrame包含多个时间序列，⽤⼀个额外的分组键的列进⾏标记
    df2 = pd.DataFrame({
        'time': times.repeat(3),
        'key': np.tile(['a', 'b', 'c'], N),
        'value': np.arange(N * 3.)
    })
    printn('第二个DF:', df2[:7])
    # 要对每个key值进⾏相同的重采样，我们引⼊pandas.Grouper对象
    time_key = pd.Grouper(freq='5min')
    # 设定时间索引，⽤key和time_key分组，然后聚合
    resampled = (df2.set_index('time').groupby(['key', time_key]).sum())
    printn('聚合后的数据:', resampled, '索引还原:', resampled.reset_index())


# timegroup()


# 链式编程计数
# 当对数据集进⾏⼀系列变换时，你可能发现创建的多个临时变量其实并没有在分析中⽤到
def chaincode():
    '''
    DataFrame.assign⽅法是⼀个df[k] = v形式的函数式的列分配⽅法。
    它不是就地修改对象，⽽是返回新的修改过的DataFrame
    下面两套语句是等价的
    # Usual non-functional way
        df2 = df.copy()
        df2['k'] = v
    # Functional assign way
        df2 = df.assign(k=v)
    '''
    df = pd.DataFrame({
        'key': np.tile(['a', 'b', 'c'], 3),
        'col1': np.random.randn(9),
        'col2': np.random.randn(9)
    })
    df2 = df[df['col2'] < 0].copy()  # 明确的告诉创建一个副本，否则出现链式索引警告
    df2['col1_demeaned'] = df2['col1'] - df2['col1'].mean()
    result = df2.groupby('key').col1_demeaned.std()
    printn("原始df:", df2, '分组计算后的结果:', result)

    # 就地分配可能会⽐assign快，但是assign可以⽅便地进⾏链式编程
    result = df2.assign(col1_demeaned=df2.col1 -
                        df2.col2.mean()).groupby('key').col1_demeaned.std()
    printn('链式编程的结果:', result)

    # assign和许多其它pandas函数可以接收类似函数的参数，即可调⽤对象（callable）
    # 借用函数写成一个单链表达式 如:df2=df[lambda x :x['col2']<0].copy()
    result = df[lambda x: x['col2'] < 0].copy().assign(
        col1_demeaned=lambda x: x.col1 - x.col1.mean()).groupby(
            'key').col1_demeaned.std()
    printn('链式编程的结果2:', result)


# chaincode()

# 管道方法
'''
你可以⽤Python内置的pandas函数和⽅法，⽤带有可调⽤对象的链式编程做许多⼯作。
但是，有时你需要使⽤⾃⼰的函数，或是第三⽅库的函数。这时就要⽤到管道⽅法。
'''


def tupemethod():
    df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b'])
    printn('初始df:', df)
    a = np.mean(df, axis=1)
    printn('对初始df运算的结果:', a)
    b = np.sum(a)
    printn('对第一次结果运算后的第二次结果:', b)

    # 使用管道方法
    # 当使⽤接收、返回Series或DataFrame对象的函数式，你可以调⽤pipe将其重写
    # pipe()，第一个参数是函数名，后面参数是传递给该函数的参数
    b = df.pipe(np.mean, axis=1).pipe(np.sum)
    printn('管道方法的结果:', b)

    # pipe的另⼀个有⽤的地⽅是提炼操作为可复⽤的函数
    df = pd.DataFrame({
        'key1': [7, 7, 7, 8, 8, 8],
        'key2': [11, 11, 15, 18, 18, 19],
        'col1': [4, 5, 6, 7, 5, 7]
    })
    printn('初始df:', df)
    g = df.groupby(['key1', 'key2'])
    # printn(g.transform('mean')['col1'])
    # transform转换出来的还是DataFrame，要用列索引转成Series;或直接索引了再transform
    df['col1'] = df['col1'] - g['col1'].transform('mean')
    printn('减去某个值后的df:', df)

    # 你想转换多列，并修改分组的键。另外，你想⽤链式编程做这个转换
    def group_demean(df, by, cols):
        result = df.copy()
        g = df.groupby(by)
        for c in cols:
            result[c] = df[c] - g[c].transform('mean')
        return result

    # 然后可以写为:
    result = (df[df.col1 < 0].pipe(group_demean, ['key1', 'key2'], ['col1']))
    printn('管道写法结果2:', result)


tupemethod()
